package com.yonyou.ypd.interceptor;

import com.yonyou.diwork.ott.RobotInvocationGetter;
import com.yonyou.diwork.permission.annotations.ButtonPermission;
import com.yonyou.diwork.permission.annotations.DiworkPermission;
import com.yonyou.diwork.service.auth.IDiWorkPermissionService;
import com.yonyou.iuap.context.InvocationInfoProxy;
import com.yonyou.iuap.metadata.core.model.businessobject.QueryBusinessObjectListReturn;
import com.yonyou.iuap.metadata.exception.MetadataApiRestException;
import com.yonyou.iuap.tenant.sdk.UserCenter;
import com.yonyou.iuap.uimeta.common.core.dto.nfw.NfwActionDTO;
import com.yonyou.iuap.uimeta.common.itf.sdk.SdkUnifiedNfwQueryItf;
import com.yonyou.iuap.uimeta.common.itf.sdk.param.QueryNfwParam;
import com.yonyou.iuap.uimeta.sdk.unified.itf.UnifiedMetaBizItf;
import com.yonyou.permission.services.SdkDynamicTokenManagerService;
import com.yonyou.ucf.mdd.auth.CommonCommandService;
import com.yonyou.ucf.mdd.common.constant.MddConstants;
import com.yonyou.ucf.mdd.common.model.uimeta.BillAction;
import com.yonyou.ucf.mdd.ext.core.AppContext;
import com.yonyou.ucf.mdd.ext.dubbo.DubboReferenceUtils;
import com.yonyou.ucf.mdd.ext.support.interfaces.IMddExtSupportService;
import com.yonyou.ucf.mdd.ext.util.DomainUtils;
import com.yonyou.ypd.bill.basic.constant.BillCommonConstant;
import com.yonyou.ypd.bill.basic.exception.YpdException;
import com.yonyou.ypd.utils.http.BillActionUrlManager;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.collections4.map.HashedMap;
import org.apache.commons.lang3.ObjectUtils;
import org.apache.commons.lang3.StringUtils;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import javax.annotation.PostConstruct;
import javax.servlet.http.HttpServletRequest;
import java.lang.reflect.Method;
import java.util.*;

/**
 * 权限过滤
 *
 * @author ypd
 * @ClassName: YpdAuthControllerAop
 * @description: YPD框架功能权限垂直越权检查
 * @date 2022/9/21  16:06
 */
@Slf4j
@Aspect
@Component
@Order(999)
public class YpdAuthControllerAop {

    @Pointcut("execution(* com.yonyou*..*.controller..*.*(..))")
    public void normalPointcutWeb() {
    }

    @Pointcut("execution(* com.yonyou.ypd.bill..controller.AbsBillController.*(..))")  //排除部分类，不过滤
    public void excludePointcutWeb() {
    }

    @Pointcut("normalPointcutWeb() && !excludePointcutWeb()")
    public void serviceApi() {
    }

    // YPD框架直接验证serviceCode的请求
    public static final String[] strYpdRefUrls = new String[]{
            "/pub/ref/getRefMeta", "/bill/ref/getRefData", "/bill/getCoordinatedRefData", "/pub/ref/savePubRefConfig", "/ypd/pub/ref/getRefMeta", "/ypd/bill/ref/getRefData"
            , "/pub/ref/restoreCheckflds", "/pub/ref/getPubRefFields", "/rank/recent/list/refdata", "/rank/recent/record/refs", "/ref/hot/batchSetHots"
            , "/option/updateOption", "/option/getOptionData", "/option/getOptionMeta", "/option/getCommonOption", "/option/getOptionValueByName"
            , "/billmeta/restoreUserSet", "/billmeta/groupset", "/billmeta/hasUserSet", "/billmeta/group", "/bizflow/batchPush", "/billrelation/map"
    };
    // YPD框架公共请求补全
    public static final String[] strYpdCommonCommands = new String[]{
            "/bill/list",
            "/bill/batchDo",
            "/bill/transferCommonRest",
            "/bill/detail",
            "/bill/querytree",
            "/billNumber/findByBillNum",
            "/bill/add",
            "/bill/export",
            "/bill/billImport",
            "/billtemp/getExportProcess",
            "/ypd/bill/meta",
            "/ypd/bill/tplAndMeta",
            "/ypd/bill/list",
            "/ypd/bill/detail",
            "/ypd/bill/querytree",
            "/ypd/bill/add",
            "/ypd/bill/export",
            "/ypd/bill/billImport",
            "/ypd/bill/copy",
            "/ypd/bill/save",
            "/ypd/bill/batchdelete",
            "/ypd/bill/submit",
            "/ypd/bill/unsubmit",
            "/ypd/bill/batchsubmit",
            "/ypd/bill/batchunsubmit",
            "/ypd/bill/audit",
            "/ypd/bill/batchaudit",
            "/ypd/bill/unaudit",
            "/ypd/bill/batchunaudit",
            "/ypd/bill/batchDo",
            "/ypd/bill/multiTrans",
            "/ypd/bill/executeFormulaCalculate"
    };

    // 需要兼容的各种billNo
    private final String[] billNoNamesToMatch = new String[]{"billno", "billnum", "billNum", "billNumber", "cBillNo"};

    private Set<String> ypdCommonCommandSet;
    private Set<String> ypdRefUrlSet;// 参照请求地址集合
    private static final String AUTHDOMAIN = "diwork-auth";

    @Autowired(required = false)
    private IDiWorkPermissionService iDiWorkPermissionService;

    @Autowired
    private SdkUnifiedNfwQueryItf unifiedNfwQueryService;

    @Autowired
    private UnifiedMetaBizItf unifiedMetaBizItf;

    @Autowired
    IMddExtSupportService mddExtSupportService;

    @Autowired
    CommonCommandService commonCommandService;

    @Autowired
    SdkDynamicTokenManagerService sdkDynamicTokenManagerService;

//    @Value("${ypd.servicecode.nonecheck.whitelist:}")
//    private String strUrlWhiteList;

    @Value("${ypd.custom.defaultCheckCommonCommandAction:}")
    private String defaultCheckCommonCommandAction;

    @Value("${ypd.billnum.nonecheck.whitelist:}")
    private String strBillnumWhiteList;

    @Value("${control_noauthid:}")
    private String strForceChkAuthidEmpty;// authid为空篡改的强制校验

    private Set<String> ypdNoneAuthCheck = new HashSet<>();// ypd框架级越权检查白名单
    //    private List<String> noneAuthCheckUrlList = null;// url跳过serviceCode越权检查的白名单
    private Set<String> billNumNoneAuthCheckUrl = null;    // url中BillNum跳过serviceCode越权检查的白名单
    private final String Val_CommonCommand = "CommonCommand";
    private static final String VAL_SERVICECODE_YB = "ybbase";
    private static final String VAL_SERVICECODE_UNDEFINED = "undefined";
    private final String[] strCommonCommandPatterns = new String[]{"/bill/asyncBatchActionQuery", "/billmeta/", "/ypd/bill/meta"
            , "/billtemp/"};// ypd异步进度查询url前缀
    private String[] ypdNoneAuthCheckList_Function = new String[]{"/api/yonscript", "/api/script", "/opencomponentsystem/public", "/web/function/invoke", "/custom/do", "/ypd/script", "/bill/custom"};

    @PostConstruct
    public void init() {
        ypdNoneAuthCheck.add("/bpm/complete");
        ypdNoneAuthCheck.add("/bill/getFeAID");// node端入口有单独的getFeAID处理
        ypdNoneAuthCheck.add("/bill/generateADT");// 审批消息等生成动态token放行
        ypdNoneAuthCheck.add("/billmeta/listCacheStatus");// listCacheStatus公共请求跳过 --yanx于2023/4/21注释
        ypdNoneAuthCheck.addAll(Arrays.asList(ypdNoneAuthCheckList_Function));

        if (StringUtils.isNotEmpty(strBillnumWhiteList)) {
            billNumNoneAuthCheckUrl = new HashSet<>(Arrays.asList(strBillnumWhiteList.split(",")));
        }

        ypdCommonCommandSet = new HashSet<>(Arrays.asList(strYpdCommonCommands));
        ypdRefUrlSet = new HashSet<>(Arrays.asList(strYpdRefUrls));
    }

    @Before("serviceApi()")
    public void beforeController(JoinPoint jp) throws Exception {
        Class<?> classTarget = jp.getTarget().getClass();
        String methodName = jp.getSignature().getName();
        Class<?>[] parameterTypes = ((MethodSignature) jp.getSignature()).getParameterTypes();
        Method objMethod = classTarget.getMethod(methodName, parameterTypes);
        // 增加权限注解判断，如果加了权限注解的，框架不再控制
        ButtonPermission buttonPermission = objMethod.getAnnotation(ButtonPermission.class);
        if (null != buttonPermission) {
            return;
        }
        DiworkPermission diworkPermission = objMethod.getAnnotation(DiworkPermission.class);
        if (null == diworkPermission) {
            diworkPermission = classTarget.getAnnotation(DiworkPermission.class);
        }
        // 类上打了权限注解不为空，直接返回不控制
        if (diworkPermission != null) {
            return;
        }

        RequestAttributes ra = RequestContextHolder.getRequestAttributes();
        ServletRequestAttributes sra = (ServletRequestAttributes) ra;
        HttpServletRequest request = null;
        if (sra != null) {
            request = sra.getRequest();
        }
        if (request == null || this.ignoreAuthRequest(request)) {// openAPI、内部请求等暂跳过权限检查 --yanx于2023/4/9注释
            return;
        }

        // 上下文为空的情况，不进行越权校验
        if(ObjectUtils.isEmpty(InvocationInfoProxy.getTenantid()) || ObjectUtils.isEmpty(InvocationInfoProxy.getUserid())) {
            return;
        }

        // 上下文中获取serviceCode
        String serviceCode = AppContext.getThreadContext(MddConstants.PARAM_SERVICE_CODE);
        if (StringUtils.isNotEmpty(serviceCode)) {
            InvocationInfoProxy.setExtendAttribute(BillCommonConstant.PARAM_SERVICECODE, serviceCode);
        }
        String transtype = request.getParameter("stranstype");// 优选判断stranstype --yanx于2023/7/4注释
        if (StringUtils.isEmpty(transtype)) {
            transtype = request.getParameter("transtype");
        }
        if (StringUtils.isNotEmpty(transtype)) {// 线程上下文存储serviceCode和交易类型 --yanx于2023/8/7注释
            InvocationInfoProxy.setExtendAttribute(BillCommonConstant.PARAM_TRANSTYPE, transtype);
        }

        String url = request.getRequestURI();
        String _contextPath = null;
        if (null != request.getServletContext()) {
            _contextPath = request.getServletContext().getContextPath();
        }
        String propKey = "ypd.auth.contextPath"; //此配置针对支持ypd和mdd越权的场景，为区分ypd还是mdd加了自定义的域名
        String ypdAuthContextPath = AppContext.getEnvConfig(propKey);
        if(ObjectUtils.isEmpty(_contextPath) && ObjectUtils.isNotEmpty(ypdAuthContextPath)) {
            _contextPath = ypdAuthContextPath;
        }
        // 替换context
        url = url.replaceFirst(_contextPath, "");
        // 1. ypd框架级越权检查白名单直接跳过 --yanx于2023/3/12注释
        if (ypdNoneAuthCheck.contains(url)) {
            return;
        }

        // 2. 恢复管理员身份放行的逻辑 --yanx于2023/3/17注释
        //  || YhtUserUtil.isAdmin(user)
        boolean isTenantAdmin = UserCenter.isTenantAdmin(InvocationInfoProxy.getUserid(), InvocationInfoProxy.getTenantid()); //判断是否管理员
        if (isTenantAdmin) {
            return;
        }

        Boolean ypdRefUrlFlag = false;
        if (ypdRefUrlSet.contains(url)) {  // 参照等请求地址直接校验传入serviceCode 标识
            ypdRefUrlFlag = true;
        }

        //特殊处理billnum，针对请求链接中携带billnum的情况
        String billnum = (String) request.getAttribute("billnum");
        if (null == billnum) {// 需要兼容的各种billNo取值 --yanx于2023/5/11注释
            billnum = this.findBillNoFromReq(request);
        }
        if (StringUtils.isEmpty(billnum) && !ypdRefUrlFlag) {// 获取不到billNo并且请求地址不是直接校验传入serviceCode，直接跳出
            log.error("越权检查发现请求参数中billnum为空, 请求uri: {}", url);
            return;
        }

        String dynamicAuthToken = this.getDynamicAuthTokenFromRequest(request);
        if (StringUtils.isNotEmpty(dynamicAuthToken)) {// 消息打开等动态token验证 --yanx于2023/4/26注释
            dynamicAuthToken = this.checkDynamicTokenAuth(dynamicAuthToken);
            if (StringUtils.isNotEmpty(dynamicAuthToken)) {
                AppContext.setThreadContext(MddConstants.DYNAMIC_AUTH_TOKEN, dynamicAuthToken);
                return;
            }
        }

        String authId;
        if (ypdRefUrlFlag) {// 参照等请求地址直接校验传入serviceCode
            authId = serviceCode;
        } else if (CollectionUtils.isNotEmpty(billNumNoneAuthCheckUrl) && billNumNoneAuthCheckUrl.contains(billnum)) {//领域单据配置billnum，直接校验传入serviceCode
            authId = serviceCode;
        } else {
            // 公共请求固定找CommonCommand的action --yanx于2023/4/8注释
            String action = request.getParameter("action");
            if (ypdCommonCommandSet.contains(url) || commonCommandService.commonCommandSet.contains(url) || commonCommandService.isCommonCommand(url)
                    || this.matchCommonCommands(url)) {// 异步轮训检查请求等匹配也归到CommonCommand --yanx于2023/4/24注释
                action = Val_CommonCommand;
            }
            String feV = request.getParameter("feV");
            authId = getAuthId(billnum, action, url, feV);
        }

        boolean forceChkAuthidEmpty = StringUtils.isEmpty(strForceChkAuthidEmpty) ? false : Boolean.valueOf(strForceChkAuthidEmpty);// 开启authid非空强制校验
        IDiWorkPermissionService iDiWorkPermissionService = DubboReferenceUtils.getDubboService(IDiWorkPermissionService.class, AUTHDOMAIN, null);
        if (StringUtils.isNotEmpty(authId)) {
            authId = tradeAuthIdWithTransType(transtype, authId);// 处理交易类型authId --yanx于2023/5/11注释
            // 没有权限判断：无权限数据或权限数据中没有authId
            boolean isNullHoldAuthItems = !iDiWorkPermissionService.checkFunAuthorityByAuthId(InvocationInfoProxy.getTenantid(), InvocationInfoProxy.getUserid(), authId);
            if (isNullHoldAuthItems) {// 无权限直接抛异常
                String errMsg = String.format(com.yonyou.iuap.ucf.common.i18n.InternationalUtils.getMessageWithDefault("UID:P_YPD-BE_18FC6F4E0470009B", "没有相应权限。 当前用户: %s, billnum: %s, authid: %s") /* "没有相应权限。 当前用户: %s, billnum: %s, authid: %s" */, InvocationInfoProxy.getUsername(), billnum, authId);//-----针对DDMPT-110834问题，进行话术优化
                throw new YpdException(errMsg);
            } else {
                // 5. 用户有authid权限通过检查
                return;
            }
        } else {
            String errMsg = String.format(com.yonyou.iuap.ucf.common.i18n.InternationalUtils.getMessageWithDefault("UID:P_YPD-BE_18FC6F4E0470009F", "越权检查未获取到权限authId, 请求uri: %s, billnum: %s") /* "越权检查未获取到权限authId, 请求uri: %s, billnum: %s" */, url, billnum);
            if (forceChkAuthidEmpty) {// 强控抛异常
                throw new YpdException(errMsg);
            }
            log.warn(errMsg);
        }

        // 8. 兜底检查serviceCode, 增加undefined特殊判断
        if (StringUtils.isNotEmpty(serviceCode) && !VAL_SERVICECODE_UNDEFINED.equals(serviceCode) && !iDiWorkPermissionService.checkFunAuthorityByAuthId(InvocationInfoProxy.getTenantid(), InvocationInfoProxy.getUserid(), serviceCode)) {
            String errMsg = com.yonyou.iuap.ucf.common.i18n.InternationalUtils.getMessageWithDefault("UID:P_YPD-BE_18FC6F4E0470009C", "当前用户没有该服务权限:") /* "当前用户没有该服务权限:" */ + serviceCode;
            if (forceChkAuthidEmpty) {
                throw new YpdException(errMsg);
            }
            log.warn(errMsg);
        }
    }

    private boolean matchCommonCommands(String url) {
        for (String commonCommandPattern : strCommonCommandPatterns) {
            if (url.contains(commonCommandPattern)) {
                return true;
            }
        }
        return false;
    }

    private String tradeAuthIdWithTransType(String transtype, String authId) {
        if (StringUtils.isNotEmpty(transtype) && !authId.startsWith(transtype)) {
            return String.format("%s_%s", transtype, authId);
        }
        return authId;
    }

    private String findBillNoFromReq(HttpServletRequest request) {
        for (String paramName : billNoNamesToMatch) {
            String billNo = request.getParameter(paramName);
            if (StringUtils.isNotEmpty(billNo)) {
                return billNo;
            }
        }

        return null;
    }

    // 如果动态权限 --yanx于2023/6/14注释
    public String checkDynamicTokenAuth(String dynamicAuthToken) {
        //增加对临时权限token的合法性验证
        if (StringUtils.isNotEmpty(dynamicAuthToken)) {
            if (!sdkDynamicTokenManagerService.validateTokenNotUseAuth(null, dynamicAuthToken)) {
                //验证不过直接返回空，异常交给下面的拦截处理，这里只输出error日志方便排查问题
                log.error("动态权限token检查失败, dynamicAuthToken:{}", dynamicAuthToken);
                return null;
            }
        }

        return dynamicAuthToken;
    }

    /**
     * 判断是否需要忽略安全拦截的请求（目前主要是openAPI、内部请求）
     *
     * @return
     */
    private boolean ignoreAuthRequest(HttpServletRequest request) {
        //判断是否内网请求
        String netWorkType = request.getHeader("x-network-type");
        if (StringUtils.isNotEmpty(netWorkType) && !netWorkType.equalsIgnoreCase("public")) {
            return true;
        }
        return false;
    }

    public String getDynamicAuthTokenFromRequest(HttpServletRequest request) {
        String dynamicAuthToken = request.getParameter("dynamic_auth_token");
        if (StringUtils.isEmpty(dynamicAuthToken)) {
            dynamicAuthToken = request.getHeader("dynamic-auth-token");
        }

        return dynamicAuthToken;
    }

    // 获取业务对象的serviceCodes列表并与当前匹配防篡改
    private boolean matchBizObjServiceCode(String serviceCode, String busiObj, String tenantId) throws MetadataApiRestException {
        QueryBusinessObjectListReturn busiObjReturn = mddExtSupportService.queryBizObjByCode(tenantId, busiObj);// 暂时使用无缓存的api,待提供serviceCodes缓存后更换api
        if (null == busiObjReturn) {
            return false;
        }
        List<String> serviceCodes = busiObjReturn.getServiceCodes();
        if (CollectionUtils.isEmpty(serviceCodes)) {
            return false;
        }
        return serviceCodes.contains(serviceCode);
    }


    /**
     * 根据特定的条件获取授权的id
     *
     * @param billNo
     * @param action
     * @param url
     * @return
     * @throws Exception
     */
    private String getAuthId(String billNo, String action, String url, String feV) throws Exception {
        String commonCommandAuthid = null;
        try {
            if (!"6".equals(feV)) {
                // 匹配action和url，只要满足一个authid不为空即返回
                Map<String, Object> params = new HashedMap();
                params.put("billNumber", billNo);
                List<BillAction> billAction = unifiedMetaBizItf.getBillAction(params);
                if (billAction == null) {
                    return null;
                }
                for (BillAction billaction : billAction) {
                    String authId = billaction.getAuthid();
                    if (StringUtils.isEmpty(authId)) {
                        continue;
                    }
                    String commandAction = billaction.getAction();
                    if (commandAction != null && commandAction.equals(action)) {
                        return authId;
                    }
                    String srvUrl = billaction.getSvcurl();
                    if (srvUrl != null && srvUrl.equals(url)) {
                        return authId;
                    }
                    if (null == commonCommandAuthid && Val_CommonCommand.equals(commandAction)) {
                        commonCommandAuthid = authId;
                    }
                }
            } else {
                String coverAction = new BillActionUrlManager().getBillActionUrlMap().get(action);
                if (ObjectUtils.isNotEmpty(coverAction)) {
                    action = coverAction;
                }
                String coverUrl = new BillActionUrlManager().getBillActionUrlMap().get(url);
                if (ObjectUtils.isNotEmpty(coverUrl)) {
                    url = coverUrl;
                }
                // 匹配action和url，只要满足一个authid不为空即返回
                QueryNfwParam param = new QueryNfwParam();
                param.setBillNo(billNo);
                param.setYhtTenantId(InvocationInfoProxy.getTenantid());
                param.setYhtUserId(InvocationInfoProxy.getUserid());
                param.setOwnDomain(DomainUtils.getAppName());
                param.setTplId(null);
                List<NfwActionDTO> commandMapList = unifiedNfwQueryService.getNfwActionList(param);
                if (CollectionUtils.isEmpty(commandMapList)) {
                    return null;
                }
                for (NfwActionDTO nfwActionDTO : commandMapList) {
                    String authId = nfwActionDTO.getAuthId();
                    if (StringUtils.isEmpty(authId)) {
                        continue;
                    }
                    String commandAction = nfwActionDTO.getActionCode();// 固定使用后端动作actionCode --yanx于2023/7/20注释
                    if (commandAction != null && commandAction.equals(action)) {
                        return authId;
                    }
                    String srvUrl = nfwActionDTO.getSvcUrl();
                    if (srvUrl != null && srvUrl.equals(url)) {
                        return authId;
                    }
                    if (null == commonCommandAuthid && Val_CommonCommand.equals(commandAction)) {
                        commonCommandAuthid = authId;
                    }
                }
            }
        } catch (Exception e) {
            log.error("越权查询action获取单据的authid异常："+ e.getMessage());
            String errMsg = String.format(com.yonyou.iuap.ucf.common.i18n.InternationalUtils.getMessageWithDefault("UID:P_YPD-BE_18FC6F4E0470009E", "越权查询action获取单据的authid异常, billnum: %s, action: %s, uri: %s") /* "越权查询action获取单据的authid异常, billnum: %s, action: %s, uri: %s" */, billNo, action, url);
            throw new YpdException(errMsg);
        }
        boolean isDefaultCheckCommonCommandAction = StringUtils.isEmpty(defaultCheckCommonCommandAction) ? false
                : Boolean.valueOf(defaultCheckCommonCommandAction);// 领域可配置是否找不到action时走CommonCommand
        if (isDefaultCheckCommonCommandAction) {
            if (StringUtils.isNotEmpty(commonCommandAuthid)) {
                return commonCommandAuthid;
            }
            action = Val_CommonCommand;
        }

        boolean forceChkAuthidEmpty = StringUtils.isEmpty(strForceChkAuthidEmpty) ? false
                : Boolean.valueOf(strForceChkAuthidEmpty);// 开启authid非空强制校验
        String errMsg = String.format(com.yonyou.iuap.ucf.common.i18n.InternationalUtils.getMessageWithDefault("UID:P_YPD-BE_18FC6F4E0470009D", "越权检查未能找到单据的authid, billnum: %s, action: %s, uri: %s") /* "越权检查未能找到单据的authid, billnum: %s, action: %s, uri: %s" */, billNo, action, url);
        if (forceChkAuthidEmpty) {// 强控抛异常
            throw new YpdException(errMsg);
        }

        // 没有权限authid记录日志，继续走后边sc检查逻辑
        log.warn(errMsg);
        return null;
    }
}
